<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
<script src="https://cdnjs.cloudflare.com/ajax/libs/echarts/4.2.1/echarts.min.js"></script>
<script src="./lib/echarts-gl.min.js"></script>
<script>


function getParam(name) {
   var reg = new RegExp("(^|&)" + name + "=([^&]*)(&|$)");
   var r = window.location.search.substr(1).match(reg);
   if (r != null) return unescape(r[2]); return null;
}


function getFloat(name) {
    return Number.parseFloat(getParam(name));
}


// 给定焦距f，根据几何关系计算相机到地面的距离
function calcZ(f, height, width, length, b, sx, sy, cols, rows) {
    // 首先根据x方向的限制条件计算安装高度
    var z1 = (width + b) * f / (sx * cols);
    // 然后根据y方向的限制条件计算安装高度
    var z2 = length * f / (sy * rows);
    return Math.max(z1, z2) + height;
}


// 计算在指定的相机参数下，对应于z的视差值
function calcDisp(f, b, sx, z) {
    return b * f / sx / z;
}


// 计算深度的误差与视差的误差的比例，即ΔZ/Δd，单位 mm/px
function calcError(f, z, b, sx) {
    return Math.pow(z, 2) / f / b * sx;
}


// 遍历所有可能的焦距，找到最优值
function calcBestF(height, width, length, b, sx, sy, cols, rows) {
    var start = 0.01;
    var end = 100;
    var step = 0.01;
    var listF = [];
    var listZ = [];
    var listE = [];
    for (var f = start; f < end; f += step) {
        var z = calcZ(f, height, width, length, b, sx, sy, cols, rows);
        var e = calcError(f, z, b, sx);
        listF.push(f);
        listZ.push(z);
        listE.push(e);
    }
    var minimum = Number.MAX_VALUE;
    var minimumIndex = -1;
    for (var i = 0; i < listF.length; i++) {
        var e = listE[i];
        if (e < minimum) {
            minimum = e;
            minimumIndex = i;
        }
    }
    var best = listF[minimumIndex];
    return [best, minimum];
}


function calcErrorXY(z, f, sx, sy) {
    return [sx / f * z, sy / f * z];
}


var myChart;


function init() {
    myChart = echarts.init(document.getElementById('div_3dplot'));
}


function plot(height, width, length, b, f, sx, sy, cols, rows, z) {
    var option = {
        tooltip: {},
        xAxis3D: {
            type: 'value',
        },
        yAxis3D: {
            type: 'value',
        },
        zAxis3D: {
            type: 'value',
            min: 0,
        },
        grid3D: {
            viewControl: {
                projection: 'projection'
            }
        },
        series: []
    };
    
    function addLine3d(data, lineWidth) {
        lineWidth = lineWidth == null ? 1 : lineWidth;
        option.series.push({
            type: 'line3D',
            data: data,
            lineStyle: {
                width: lineWidth
            }
        });
    }
    
    // 生成一个指定高度上的矩形
    function rect(x, y, z, w, h) {
        var hw = w / 2, hh = h / 2;
        var ret = [[x - hw, y - hh, z], [x - hw, y + hh, z], [x + hw, y + hh, z], [x + hw, y - hh, z], [x - hw, y - hh, z]];
        addLine3d(ret);
        return ret;
    }
    
    function line(x0, y0, z0, x1, y1, z1) {
        var ret = [[x0, y0, z0], [x1, y1, z1]];
        addLine3d(ret);
        return ret;
    }

    // 测量空间
    var x0 = width / 2, y0 = length / 2;
    var line1 = rect(0, 0, 0, width, length);
    var line2 = rect(0, 0, height, width, length);
    var line3 = line(x0, y0, 0, x0, y0, height);
    var line4 = line(x0, -y0, 0, x0, -y0, height);
    var line5 = line(-x0, y0, 0, -x0, y0, height);
    var line6 = line(-x0, -y0, 0, -x0, -y0, height);
    
    // 相机芯片
    var z_chip = z + f, wx_chip = sx * cols; wy_chip = sy * rows, halfB = b / 2;
    var rect3 = rect(-halfB, 0, z_chip, wx_chip, wy_chip);
    var rect4 = rect(halfB, 0, z_chip, wx_chip, wy_chip);
    
    // 相机视场
    function fieldLine(point1, origin) {
        var x1 = point1[0], y1 = point1[1], z1 = point1[2];
        var ox = origin[0], oy = origin[1], oz = origin[2];
        var lambda = -oz / (oz - z1);
        var ret =  [[x1, y1, z1], [ox + lambda * (ox - x1), oy + lambda * (oy - y1), oz + lambda * (oz - z1)]];
        addLine3d(ret);
        return ret;
    }
    var fieldLines1 = [];
    var fieldLines2 = [];
    var minX = Number.MAX_VALUE;
    var maxX = Number.MIN_VALUE;
    var minY = Number.MAX_VALUE;
    var maxY = Number.MIN_VALUE;
    for (var i = 0; i < 4; i++) {
        fieldLines1.push(fieldLine(rect3[i], [-halfB, 0, z]));
        fieldLines2.push(fieldLine(rect4[i], [halfB, 0, z]));
        minX = Math.min(minX, fieldLines1[i][1][0]);
        maxX = Math.max(maxX, fieldLines2[i][1][0]);
        minY = Math.min(minY, fieldLines1[i][1][1]);
        maxY = Math.max(maxY, fieldLines1[i][1][1]);
    }
    
    var rangeX = maxX - minX;
    var rangeY = maxY - minY;
    var rangeZ = (z + f);
    var range = Number.parseInt(Math.max(rangeX, rangeY, rangeZ) * 1.4);
    option.xAxis3D.min = option.yAxis3D.min = - range / 2;
    option.xAxis3D.max = option.yAxis3D.max = range / 2;
    option.zAxis3D.max = range;
    myChart.setOption(option);
}


function report() {
    var reportDiv = document.getElementById('div_report');
    var height = getFloat('height');
    var width = getFloat('width');
    var length = getFloat('length');
    var b = getFloat('b');
    var f = getFloat('f');
    var sx = getFloat('sx') / 1000;
    var sy = getFloat('sy') / 1000;
    var rows = getFloat('rows');
    var cols = getFloat('cols');
    
    var ret = calcBestF(height, width, length, b, sx, sy, cols, rows);
    var bestF = ret[0];
    var minError = ret[1];
    
    var z = calcZ(f, height, width, length, b, sx, sy, cols, rows);
    var dispMin = calcDisp(f, b, sx, z);
    var dispMax = calcDisp(f, b, sx, z - height);
    var error1 = calcError(f, z - height, b, sx);
    var error2 = calcError(f, z, b, sx);
    var errorXY1 = calcErrorXY(z - height, f, sx, sy);
    var errorXY2 = calcErrorXY(z, f, sx, sy);
    
    var reportText = 
        `理论上焦距为${bestF.toFixed(2)} mm 时z轴误差最小，这样最大的z轴误差为${minError.toFixed(2)} mm/px。
        你选择了焦距为${f} mm 的镜头，这样测量空间的深度范围是${(z - height).toFixed(2)} ~ ${z.toFixed(2)} mm，
        对应的z轴误差范围是${error1.toFixed(2)} ~ ${error2.toFixed(2)} mm/px;
        对应的x轴误差范围是${errorXY1[0].toFixed(2)} ~ ${errorXY2[0].toFixed(2)} mm/px;
        对应的y轴误差范围是${errorXY1[1].toFixed(2)} ~ ${errorXY2[1].toFixed(2)} mm/px;
        对应的视差范围是${dispMin.toFixed(2)} ~ ${dispMax.toFixed(2)} px，需要计算的视差个数是${(dispMax - dispMin + 1).toFixed(2)} px。`;
    reportDiv.innerText = reportText;
    plot(height, width, length, b, f, sx, sy, cols, rows, z);
}

</script>
</head>
<body onload="init();report();">
<div id="div_report">
</div>
<div id="div_3dplot" style="width: 600px; height:600px;"></div>
</body>
</html>